// Room --------------------------------
function jq(myid) {
  return '#' + myid.replace(/(@|:|\.|\[|\]|,)/g, '\\$1');
}

function writeFmtpLine(fmtpObj) {
  if (!fmtpObj.hasOwnProperty('pt') || !fmtpObj.hasOwnProperty('params')) {
    return null;
  }
  var pt = fmtpObj.pt;
  var params = fmtpObj.params;
  var keyValues = [];
  var i = 0;
  for (var key in params) {
    keyValues[i] = key + '=' + params[key];
    ++i;
  }
  if (i === 0) {
    return null;
  }
  return 'a=fmtp:' + pt.toString() + ' ' + keyValues.join('; ');
}

function parseFmtpLine(fmtpLine) {
  var fmtpObj = {};
  var spacePos = fmtpLine.indexOf(' ');
  var keyValues = fmtpLine.substring(spacePos + 1).split('; ');
  var pattern = new RegExp('a=fmtp:(\\d+)');
  var result = fmtpLine.match(pattern);
  if (result && result.length === 2) {
    fmtpObj.pt = result[1];
  } else {
    return null;
  }
  var params = {};
  for (var i = 0; i < keyValues.length; ++i) {
    var pair = keyValues[i].split('=');
    if (pair.length === 2) {
      params[pair[0]] = pair[1];
    }
  }
  fmtpObj.params = params;
  return fmtpObj;
}
function getCodecPayloadType(sdpLines, codec) {
  var index = findLine(sdpLines, 'a=rtpmap', codec);
  return index ? getCodecPayloadTypeFromLine(sdpLines[index]) : null;
}
function getCodecPayloadTypeFromLine(sdpLine) {
  var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
  var result = sdpLine.match(pattern);
  return result && result.length === 2 ? result[1] : null;
}
function setCodecParam(sdp, codec, param, value) {
  var sdpLines = sdp.split('\r\n');
  var fmtpLineIndex = findFmtpLine(sdpLines, codec);
  var fmtpObj = {};
  if (fmtpLineIndex === null) {
    var index = findLine(sdpLines, 'a=rtpmap', codec);
    if (index === null) {
      return sdp;
    }
    var payload = getCodecPayloadTypeFromLine(sdpLines[index]);
    fmtpObj.pt = payload.toString();
    fmtpObj.params = {};
    fmtpObj.params[param] = value;
    sdpLines.splice(index + 1, 0, writeFmtpLine(fmtpObj));
  } else {
    fmtpObj = parseFmtpLine(sdpLines[fmtpLineIndex]);
    fmtpObj.params[param] = value;
    sdpLines[fmtpLineIndex] = writeFmtpLine(fmtpObj);
  }
  sdp = sdpLines.join('\r\n');
  return sdp;
}

function findFmtpLine(sdpLines, codec) {
  var payload = getCodecPayloadType(sdpLines, codec);
  return payload ? findLine(sdpLines, 'a=fmtp:' + payload.toString()) : null;
}

function setDefaultCodec(mLine, payload) {
  var elements = mLine.split(' ');

  // Just copy the first three parameters; codec order starts on fourth.
  var newLine = elements.slice(0, 3);

  // Put target payload first and copy in the rest.
  newLine.push(payload);
  for (var i = 3; i < elements.length; i++) {
    if (elements[i] !== payload) {
      newLine.push(elements[i]);
    }
  }
  return newLine.join(' ');
}

function maybePreferCodec(sdp, type, dir, codec) {
  var str = type + ' ' + dir + ' codec';
  if (codec === '') {
    return sdp;
  }


  var sdpLines = sdp.split('\r\n');

  // Search for m line.
  var mLineIndex = findLine(sdpLines, 'm=', type);
  if (mLineIndex === null) {
    return sdp;
  }
  // If the codec is available, set it as the default in m line.
  var payload = getCodecPayloadType(sdpLines, codec);
  if (payload) {
    sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
  }

  sdp = sdpLines.join('\r\n');
  return sdp;
}

function maybeSetVideoSendInitialBitRate(sdp, params) {
  var initialBitrate = params.videoSendInitialBitrate;
  if (!initialBitrate) {
    return sdp;
  }
  var maxBitrate = initialBitrate;
  var bitrate = params.videoSendBitrate;
  if (bitrate) {
    if (initialBitrate > bitrate) {
      initialBitrate = bitrate;
      params.videoSendInitialBitrate = initialBitrate;
    }
    maxBitrate = bitrate;
  }
  var sdpLines = sdp.split('\r\n');
  var mLineIndex = findLine(sdpLines, 'm=', 'video');
  if (mLineIndex === null) {
    return sdp;
  }
  sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-min-bitrate', params.videoSendInitialBitrate.toString());
  sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-max-bitrate', maxBitrate.toString());
  return sdp;
}

function findLine(sdpLines, prefix, substr) {
  return findLineInRange(sdpLines, 0, -1, prefix, substr);
}
function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
  var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
  for (var i = startLine; i < realEndLine; ++i) {
    if (sdpLines[i].indexOf(prefix) === 0) {
      if (!substr || sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
        return i;
      }
    }
  }
  return null;
}

function maybeSetVideoSendBitRate(sdp, bitrate) {
  return preferBitRate(sdp, bitrate, 'video');
}

function preferBitRate(sdp, bitrate, mediaType) {
  var sdpLines = sdp.split('\r\n');
  var mLineIndex = findLine(sdpLines, 'm=', mediaType);
  if (mLineIndex === null) {
    return sdp;
  }
  var nextMLineIndex = findLineInRange(sdpLines, mLineIndex + 1, -1, 'm=');
  if (nextMLineIndex === null) {
    nextMLineIndex = sdpLines.length;
  }
  var cLineIndex = findLineInRange(sdpLines, mLineIndex + 1, nextMLineIndex, 'c=');
  if (cLineIndex === null) {
    return sdp;
  }
  var bLineIndex = findLineInRange(sdpLines, cLineIndex + 1, nextMLineIndex, 'b=AS');
  if (bLineIndex) {
    sdpLines.splice(bLineIndex, 1);
  }
  var bwLine = 'b=AS:' + bitrate;
  sdpLines.splice(cLineIndex + 1, 0, bwLine);
  sdp = sdpLines.join('\r\n');
  return sdp;
}

function filterStats(stats) {
  var sender = false;
  for (var key in stats) {
    if (key.endsWith('send') && key.startsWith('ssrc')) {
      sender = true;
      break;
    }
  }
  for (key in stats) {
    if (key.startsWith('googTrack') ||
      key.startsWith('googLibjingleSession') ||
      key.startsWith('Channel') ||
      key.startsWith('Cand')) {
      delete stats[key];
    } else if (key.startsWith('ssrc') && sender && key.endsWith('recv')) delete stats[key];
    else if (key.startsWith('Conn') && stats[key].googActiveConnection !== 'true') delete stats[key];
  }
  return stats;
}

function Room(kurento, options) {

  var that = this;

  that.name = options.room;
  that.user = options.user;
  that.options = options;
  var ee = new EventEmitter();
  var streams = {};
  var participants = {};
  var participantsSpeaking = [];
  var connected = false;
  var localParticipant;
  var subscribeToStreams = options.subscribeToStreams || false;
  var updateSpeakerInterval = options.updateSpeakerInterval || 1500;
  var thresholdSpeaker = options.thresholdSpeaker || -50;

  setInterval(updateMainSpeaker, updateSpeakerInterval);

  function updateMainSpeaker() {
    if (participantsSpeaking.length > 0) {
      ee.emitEvent('update-main-speaker', [
{
        participantId: participantsSpeaking[participantsSpeaking.length - 1]
      }
]);
    }
  }

  this.reportInfo = function (type, data) {
    ee.emitEvent('info-reported', [
{
      info: {
        name: that.name,
        user: that.user,
        type: type,
        time: new Date().toLocaleString(),
        data: data
      }
    }
]);
  };

  this.getLocalParticipant = function () {
    return localParticipant;
  };

  this.addEventListener = function (eventName, listener) {
    ee.addListener(eventName, listener);
  };

  this.emitEvent = function (eventName, eventsArray) {
    ee.emitEvent(eventName, eventsArray);
  };

  this.parseStreams = function (participant, msg, stream_cb) {
    var options = msg;
    if (options.streams) {
      for (var i = 0; i < options.streams.length; i++) {
        var params = options.streams[i].params;
        if (params) {
          options.streams[i].recvAudio = params.audio == undefined ? true : params.audio;
          options.streams[i].recvVideo = params.video == undefined ? true : params.video;
        }
        var streamOpts = {
          id: options.streams[i].streamId,
          participant: participant,
          recvVideo: options.streams[i].recvVideo == undefined ? true : options.streams[i].recvVideo,
          recvAudio: options.streams[i].recvAudio == undefined ? true : options.streams[i].recvAudio,
          room: options.room,
          user: options.user,
          acodec: options.streams[i].params.acodec,
          vcodec: options.streams[i].params.vcodec,
          analysis: options.streams[i].analysis
        };

        var stream = new Stream(kurento, false, that, streamOpts);
        //participant.addStream(stream);
        //streamsOpts.push(streamOpts);
        if (stream_cb) {
          stream_cb(stream);
        }
      }
    }
  };
  this.getInfo = function () {
    return {
      device: 'browser',
      version: '0.1.0.32'
    };
  };
  this.unshare = function (room) {
    kurento.sendRequest('unshareRoom', {
      userId: options.user,
      roomId: room,
      from: that.name
    }, function (error, response) {
      if (error) {
        ee.emitEvent('error-room', [
{
          error: error
        }
]);
      } else {
        console.log('unshareRoom response :' + JSON.stringify(response));
      }
    });
  };
  this.share = function (room) {
    kurento.sendRequest('shareRoom', {
      userId: options.user,
      roomId: room,
      from: that.name,
      info: this.getInfo()
    }, function (error, response) {
      if (error) {
        ee.emitEvent('error-room', [
{
          error: error
        }
]);
      } else {
        console.log('shareRoom response : ' + JSON.stringify(response));
        var exParticipants = response.value;
        var roomEvent = {
          participants: [],
          streams: []
        };
        //exParticipants = null;
        if (exParticipants) {

          var length = exParticipants.length;
          for (var i = 0; i < length; i++) {
            if (!participants[exParticipants[i]]) {
              var participant = new Participant(kurento, false, that,
                exParticipants[i]);
              participants[participant.getID()] = participant;
            }
            roomEvent.participants.push(participant);
            that.parseStreams(participant, exParticipants[i], function (stream) {
              participant.addStream(stream);
              roomEvent.streams.push(stream);
              if (subscribeToStreams) {
                stream.subscribe();
              }
            });
          }
        }

        ee.emitEvent('room-connected', [roomEvent]);
      }
    });
  };

  this.sendMessage = function (message, to) {
    kurento.sendRequest('sendMessage', {
      roomId: options.room,
      userId: options.user,
      msg: message,
      to: to
    }, function (error, response) {
      if (error) {

      } else {

      }
    });
  };

  this.connect = function () {

    kurento.sendRequest('joinRoom', {
      userId: options.user,
      roomId: options.room,
      info: this.getInfo()
    }, function (error, response) {
      if (error) {
        ee.emitEvent('error-room', [
{
          error: error
        }
]);
        //console.error(error);
      } else {

        connected = true;

        console.log('joinRoom response : ' + JSON.stringify(response));
        var exParticipants = response.value;
        that.iceservers = response.iceservers;
        var roomEvent = {
          participants: [],
          streams: []
        };
        //exParticipants = null;
        if (exParticipants) {

          var length = exParticipants.length;
          for (var i = 0; i < length; i++) {

            var participant = new Participant(kurento, false, that,
              exParticipants[i]);

            participants[participant.getID()] = participant;

            roomEvent.participants.push(participant);
            that.parseStreams(participant, exParticipants[i], function (stream) {
              participant.addStream(stream);
              roomEvent.streams.push(stream);
              if (subscribeToStreams) {
                stream.subscribe();
              }
            });

          }
        }

        ee.emitEvent('room-connected', [roomEvent]);
      }
    });
  };


  this.subscribe = function (stream) {
    stream.subscribe();
  };

  this.onParticipantPublished = function (options) {
    if (options.userId == that.user) {
      return;
    }
    var participant = participants[options.userId];
    if (!participant) {
      participant = new Participant(kurento, false, that, options);
      participants[participant.getID()] = participant;
    }
    if (!options.streams) {
      options.streams = [];
      options.streams.push({
        streamId: options.streamId,
        params: options.params,
        analysis: options.analysis
      });
    }
    that.parseStreams(participant, options, function (stream) {
      participant.addStream(stream);
      if (subscribeToStreams) {
        stream.subscribe();
      }
    });
    ee.emitEvent('participant-published', [
{
      participant: participant
    }
]);
  };

  this.onParticipantUnpublished = function (msg) {
    msg.name = msg.name || msg.userId;
    if (msg.name == that.user) return;
    var participant = participants[msg.name];
    msg.streams = msg.streams || [];
    if (msg.streamId) {
      msg.streams.push(msg.streamId);
    }

    if (participant !== undefined) {
      ee.emitEvent('participant-unpublished', [
{
        participant: participant
      }
]);
      for (var idx in msg.streams) {
        var streamId = msg.streams[idx];
        var stream = participant.getStream(streamId);
        ee.emitEvent('stream-removed', [{stream: stream}]);
        participant.removeStream(streamId);
      }
      var streams = participant.getStreams();
      for (var key in streams) {
        ee.emitEvent('stream-removed', [
{
          stream: streams[key]
        }
]);
      }
      participant.dispose();
    } else {
      console.warn('Participant ' + msg.name +
        ' unknown. Participants: ' +
        JSON.stringify(participants));
    }
  };
  this.onReconnecting = function () {
    ee.emitEvent('reconnecting', [
{
      room: that
    }
]);
  };
  this.onParticipantJoined = function (msg) {
    var participant = participants[msg.userId];
    if (!participant) {
      participant = new Participant(kurento, false, that, msg);
      participants[participant.getID()] = participant;
    }
    ee.emitEvent('participant-joined', [
{
      participant: participant
    }
]);
  };

  this.onStreamDisconnect = function (msg) {
    msg.name = msg.name || msg.userId;
    if (msg.name != that.user) return;
    var participant = that.getLocalParticipant();
    if (msg.streamId) {
      var stream = participant.getStream(msg.streamId);
      if (stream) {
        stream.onStreamDisconnect();
      }
    }
  };
  this.onAnalyzeData = function (msg) {
    msg.name = msg.name || msg.hostId || msg.userId;
    if (msg.userId != that.user) return;

    var participant = participants[msg.name];
    if (msg.streamId) {
      var stream = participant.getStream(msg.streamId);
      if (stream) {
        stream.onAnalyzeData(msg);
      }
    }
  };

  this.onStartAnalyze = function (msg) {
    var participant = participants[msg.userId];
    var stream = participant.getStream(msg.streamId);
    if (stream) {
      stream.onStartAnalyze(msg);
    }
  };

  this.onStopAnalyze = function (msg) {
    var participant = participants[msg.userId];
    var stream = participant.getStream(msg.streamId);
    if (stream) {
      stream.onStopAnalyze(msg);
    }
  };

  this.onAnalyzeError = function (msg) {
    var participant = participants[msg.userId];
    var stream = participant.getStream(msg.streamId);
    if (stream) {
      stream.onAnalyzeError(msg);
    }
  };

  this.onParticipantLeft = function (msg) {
    msg.name = msg.name || msg.userId;
    if (msg.name == that.user) return;
    var participant = participants[msg.name];

    if (participant !== undefined) {
      delete participants[msg.name];

      ee.emitEvent('participant-left', [
{
        participant: participant
      }
]);

      var streams = participant.getStreams();
      for (var key in streams) {
        ee.emitEvent('stream-removed', [
{
          stream: streams[key]
        }
]);
      }

      participant.dispose();
      delete participants[msg.name];
    } else {
      console.warn('Participant ' + msg.name +
        ' unknown. Participants: ' +
        JSON.stringify(participants));
    }
  };

  this.onParticipantEvicted = function (msg) {
    ee.emitEvent('participant-evicted', [
{
      localParticipant: localParticipant
    }
]);
  };

  this.onNewMessage = function (msg) {
    console.log('New message: ' + JSON.stringify(msg));
    msg.room = msg.room || msg.roomId;
    msg.user = msg.user || msg.userId;
    msg.message = msg.message || msg.msg;
    var room = msg.room;
    var user = msg.user;
    var message = msg.message;

    if (user !== undefined) {
      ee.emitEvent('new-message', [
{
        room: room,
        user: user,
        message: message
      }
]);
    } else {
      console.error('User undefined in new message:', msg);
    }
  };

  this.recvIceCandidate = function (msg) {
    msg.endpointName = msg.endpointName || msg.hostId || msg.userId;
    var candidate = msg.candidate;
    var participant = participants[msg.endpointName];
    if (!participant) {
      console.error('Participant not found for endpoint ' +
        msg.endpointName + '. Ice candidate will be ignored.',
        candidate);
      return false;
    }
    var streams = participant.getStreams();
    for (var key in streams) {
      console.log('key ' + key + ' vs ' + msg.streamId);
      if (key != msg.streamId) continue;
      var stream = streams[key];
      stream.getWebRtcPeer().addIceCandidate(candidate, function (error) {
        if (error) {
          var errorMsg = 'Error adding candidate for ' + key +
            ' stream of endpoint ' + msg.endpointName +
            ': ' + error;
          console.error(errorMsg);
          that.reportInfo('error', errorMsg);
          return;
        } 
          that.reportInfo('addIceCandidate', candidate);
        
      });
    }
  };

  this.onRoomClosed = function (msg) {
    console.log('Room closed: ' + JSON.stringify(msg));
    msg.room = msg.room || msg.roomId;
    var room = msg.room;
    if (room !== undefined) {
      ee.emitEvent('room-closed', [
{
        room: room
      }
]);
    } else {
      console.error('Room undefined in on room closed', msg);
    }
  };

  this.onMediaError = function (params) {
    console.error('Media error: ' + JSON.stringify(params));
    var error = params.error;
    if (error) {
      ee.emitEvent('error-media', [
{
        error: error
      }
]);
    } else {
      console.error('Received undefined media error. Params:', params);
    }
  };

  this.leave = function (forced) {
    forced = !!forced;
    console.log('Leaving room (forced=' + forced + ')');
    if (connected && !forced) {
      kurento.sendRequest('leaveRoom', {
        roomId: options.room,
        userId: options.user
      }, function (error, response) {
        if (error) {
          console.error(error);
        } else {
          connected = false;
        }
      });
    }

    for (var key in participants) {
      participants[key].dispose();
    }
  };

  this.disconnect = function (stream) {
    var participant = stream.getParticipant();
    if (!participant) {
      console.error('Stream to disconnect has no participant', stream);
      return false;
    }

    delete participants[participant.getID()];
    participant.dispose();

    if (participant === localParticipant) {
      console.log('Unpublishing my media (I\'m ' + participant.getID() + ')');
      //delete localParticipant;
      kurento.sendRequest('unpublishStream', {
        roomId: options.room,
        userId: options.user,
        streamId: stream.streamId
      }, function (error, response) {
        if (error) {
          console.error(error);
        } else {
          console.info('Media unpublished correctly');
        }
      });
    } else {
      console.log('Unsubscribing from ' + stream.getGlobalID());
      kurento.sendRequest('unsubscribeStream', {
        sender: stream.getGlobalID(),
        roomId: options.room,
        userId: options.user,
        streamId: stream.streamId
      },
        function (error, response) {
          if (error) {
            console.error(error);
          } else {
            console.info('Unsubscribed correctly from ' + stream.getGlobalID());
          }
        });
    }
  };

  this.getStreams = function () {
    return streams;
  };

  this.addParticipantSpeaking = function (participantId) {
    participantsSpeaking.push(participantId);
  };

  this.removeParticipantSpeaking = function (participantId) {
    var pos = -1;
    for (var i = 0; i < participantsSpeaking.length; i++) {
      if (participantsSpeaking[i] == participantId) {
        pos = i;
        break;
      }
    }
    if (pos != -1) {
      participantsSpeaking.splice(pos, 1);
    }
  };

  options.id = options.user;
  localParticipant = new Participant(kurento, true, that, options);
  participants[options.user] = localParticipant;
}

// Participant --------------------------------
function deepcopy(source) {
  var result = {};
  for (var key in source) {
    result[key] = typeof source[key] === 'object' ? deepcopy(source[key]) : source[key];
  }
  return result;
}

function Participant(kurento, local, room, _options) {
  var options = deepcopy(_options);
  for (var key in _options) console.log('new Participant ' + JSON.stringify(options));
  options.user = options.user || options.userId;
  options.room = options.room || options.roomId;
  options.id = options.id || options.user;
  options.streams = options.streams || [];
  if (options.streamId) {
    options.streams = [
{streamId: options.streamId,
recvVideo: options.params.video,
recvAudio: options.params.audio}
];
  }
  var that = this;
  var id = options.id;
  var roomName = options.room;
  var userName = options.user;
  var streams = {};
  var ss = null;
  var streamsOpts = [];
  console.log('New ' + (local ? 'local ' : 'remote ') + 'participant ' + id +
    ', streams opts: ', streamsOpts);

  that.setId = function (newId) {
    id = newId;
  };

  function addStream(stream) {
    streams[stream.getGlobalID()] = stream;
    ss = stream;
    room.getStreams()[stream.getGlobalID()] = stream;
  }

  that.addStream = addStream;

  that.getStreams = function () {
    return streams;
  };

  that.getStream = function (streamId) {
    return streams[streamId];
  };

  that.removeStream = function (streamId) {
    var stream = streams[streamId];
    if (stream) {
      stream.dispose();
      delete streams[streamId];
    }
  };

  that.dispose = function () {
    for (var key in streams) {
      streams[key].dispose();
    }
  };

  that.getID = function () {
    return id;
  };

  this.sendIceCandidate = function (streamId, candidate, level) {
    console.debug(local ? 'Local' : 'Remote', 'candidate for',
      that.getID(), JSON.stringify(candidate));

    room.reportInfo('onIceCandidate', candidate);

    kurento.sendRequest('addIceCandidate', {
      roomId: options.room,
      userId: room.user,
      streamId: streamId,
      endpointName: that.getID(),
      candidate: candidate, //.candidate,
      level: level
    }, function (error, response) {
      if (error) {
        console.error('Error sending ICE candidate: ' +
          JSON.stringify(error));
      }
    });
  };
}

// Stream --------------------------------

/*
 * options: name: XXX data: true (Maybe this is based on webrtc) audio: true,
 * video: true, url: "file:///..." > Player screen: true > Desktop (implicit
 * video:true, audio:false) audio: true, video: true > Webcam
 *
 * stream.hasAudio(); stream.hasVideo(); stream.hasData();
 */
function Stream(kurento, local, room, options) {

  var that = this;

  if (options.audio == undefined) {
    options.audio = true;
  }
  if (options.video == undefined) {
    options.video = true;
  }
  if (options.screen) {
    options.video = true;
    options.audio = false;
  }
  this.streamId = options.id;
  that.acodec = options.acodec;
  that.vcodec = options.vcodec;
  that.room = room;
  that.local = local;
  that.bridged = false;
  that.level = undefined;
  that.user = room.user;
  that.externalOptions = {};
  that.analysis = options.analysis;
  var ee = new EventEmitter();
  var sdpOffer;
  var wrStream;
  var wp;
  var id;
  if (options.id) {
    id = options.id;
  } else if (options.screen) {
    id = 'screen';
  } else {
    id = 'webcam';
  }
  var video;
  var interval = undefined;
  /*    setInterval(function () {
        if (wp !== undefined) {
          var reportPC = wp.peerConnection;
  
          reportPC.getStats(null, function (stats) {
            room.reportInfo('getStats', {
              iceConnectionState: reportPC.iceConnectionState,
              iceGatheringState: reportPC.iceGatheringState,
              stats: filterStats(stats)
            });
          })
        }
      }, 3000);
      */
  this.collectStats = function (inter) {
    if (interval != undefined) clearInterval(interval);
    interval = setInterval(() => {
      if (wp !== undefined) {
        var pc = wp.peerConnection;
        pc.getStats(null, stats => {
          ee.emitEvent('stream-stats', [
{stream: that,
stats: stats}
]);
        });
      }
    }, inter);
  };
  this.startBridge = function (url) {
    that.url = url;
    if (that.bridged) {
      return;
    }
    kurento.sendRequest('startBridge', {
      roomId: room.name,
      userId: room.user,
      streamId: that.getGlobalID(),
      options: {
        rtmpUrl: url
      }
    }, function (error, response) {
      if (error) {
        console.error('Error on publishVideo: ' + JSON.stringify(error));
      } else {
        that.bridged = true;
        that.room.emitEvent('stream-bridged', [{stream: that}]);
      }
    });
  };
  this.stopBridge = function () {
    if (!that.bridged) {
      return;
    }
    kurento.sendRequest('stopBridge', {
      roomId: room.name,
      //userId : room.user,
      streamId: that.getGlobalID()
    }, function (error, response) {
      if (error) {
        console.error('Error on publishVideo: ' + JSON.stringify(error));
      } else {
        that.bridged = false;
        that.room.emitEvent('stream-unbridged', [{stream: that}]);
      }
    });
  };
  this.startRecord = function (url) {
    if (that.recording) {
      return;
    }
    kurento.sendRequest('startRecord', {
      roomId: room.name,
      streamId: that.getGlobalID(),
      options: {
        userId: room.user
      }
    }, function (error, response) {
      if (error) {
        console.log('Error on startRecord : ' + JSON.stringify(error));
      } else {
        that.recording = true;
        that.path = response.path;
        that.host = response.host;
        that.location = response.location;
        that.recordId = response.id;
        that.room.emitEvent('stream-start-record', [{stream: that}]);
      }
    });
  };
  this.stopRecord = function (url) {
    if (!that.recording) {
      return;
    }
    kurento.sendRequest('stopRecord', {
      roomId: room.name,
      streamId: that.getGlobalID()
    }, function (error, response) {
      that.recording = false;
      that.room.emitEvent('stream-stop-record', [{stream: that}]);
    });
  };
  this.onStreamDisconnect = function () {
    ee.emitEvent('stream-disconnected', [{stream: that}]);
  };

  this.onAnalyzeData = function (msg) {
    ee.emitEvent('analyze-data', [
{stream: that,
data: msg.data}
]);
  };

  this.onStartAnalyze = function (msg) {
    that.analysis[msg.options.type] = msg.options;
    ee.emitEvent('start-analyze', [
{stream: that,
analyze: msg.options}
]);
  };

  this.onAnalyzeError = function (msg) {
    ee.emitEvent('analyze-error', [
{stream: that,
analyze: msg.options}
]);
  };

  this.onStopAnalyze = function (msg) {
    delete that.analysis[msg.options.type];
    ee.emitEvent('stop-analyze', [
{stream: that,
analyze: msg.options}
]);
  };
  //var videoElements = [];
  //var elements = [];
  var participant = options.participant;

  var speechEvent;
  options.recvVideo = options.recvVideo == undefined ? options.video : options.recvVideo;
  options.recvAudio = options.recvAudio == undefined ? options.audio : options.recvAudio;
  that.options = options;
  that.options.bitrate = that.options.bitrate || 500;
  var recvVideo = options.recvVideo;
  this.getRecvVideo = function () {
    return recvVideo;
  };

  var recvAudio = options.recvAudio;
  this.getRecvAudio = function () {
    return recvAudio;
  };

  var showMyRemote = false;
  this.subscribeToMyRemote = function () {
    showMyRemote = true;
  };
  this.displayMyRemote = function () {
    return showMyRemote;
  };

  var localMirrored = false;
  this.mirrorLocalStream = function (wr) {
    showMyRemote = true;
    localMirrored = true;
    if (wr) wrStream = wr;
  };
  this.isLocalMirrored = function () {
    return localMirrored;
  };

  this.getWrStream = function () {
    return wrStream;
  };

  this.getWebRtcPeer = function () {
    return wp;
  };

  this.addEventListener = function (eventName, listener) {
    ee.addListener(eventName, listener);
  };

  this.getID = function () {
    return id;
  };

  this.getParticipant = function () {
    return participant;
  };

  this.getGlobalID = function () {
    if (that.streamId) {
      return that.streamId;
    }
    if (participant) {
      that.streamId = participant.getID() + '_' + id;
      return that.streamId;
    }
    that.streamId = id + '_webcam';
    if (that.options.screen) {
      that.streamId = id + '_screen';
    }
    return that.streamId;
  };

  this.init = function () {
    participant.addStream(that);
    var constraints = {
      audio: options.audio,
      video: {
        mandatory: {
          maxWidth: options.width ? options.width : 640,
          minWidth: options.width ? options.width : 640,
          maxHeight: options.height ? options.height : 480,
          minHeight: options.height ? options.height : 480
        },
        optional: [
          {maxFrameRate: 15},
          {minFrameRate: 15}
        ]
      }
    };
    if (!options.video) {
      constraints.video = undefined;
    }
    if (options.screen) {
      var screen_constraints = {
        video: {
          mandatory: {
            chromeMediaSource: 'desktop',
            maxWidth: 960,
            minFrameRate: 6,
            maxFrameRate: 25
          }
        }
      };
      chrome.runtime.sendMessage('pgcegdmbieahomlmpndiddpcpiaiiano',
        {
          getStream: 1
        }, function (stream) {
          screen_constraints.video.mandatory.chromeMediaSourceId = stream.streamId;
          navigator.mediaDevices.getUserMedia(screen_constraints).then(function (userStream) {
            wrStream = userStream;
            that.wrStream = wrStream;
            ee.emitEvent('access-accepted', [{stream: that}]);
          })
.catch(function (error) {
            console.error('Access denied', error);
            ee.emitEvent('access-denied', [{stream: that}]);
          });
        });
    } else {
      navigator.mediaDevices.getUserMedia(constraints).then(function (userStream) {
        wrStream = userStream;
        that.wrStream = wrStream;
        ee.emitEvent('access-accepted', [{stream: that}]);
      })
.catch(function (error) {
        console.error('Access denied', error);
        ee.emitEvent('access-denied', [{stream: that}]);
      });
    }
  };

  this.publishVideoCallback = function (error, sdpOfferParam, wp) {
    if (error) {
      return console.error('(publish) SDP offer error: ' +
        JSON.stringify(error));
    }

    room.reportInfo('createOffer', sdpOfferParam);

    console.log('Sending SDP offer to publish as ' +
      that.getGlobalID(), sdpOfferParam);
    kurento.sendRequest('publishStream', {
      roomId: room.name,
      userId: room.user,
      streamId: that.getGlobalID(),
      options: {
        sdpOffer: sdpOfferParam,
        bridge: options.bridge,
        mix: options.mix,
        record: options.record,
        audio: options.audio,
        video: options.video,
        rtmpUrl: options.rtmpUrl,
        mixerUrl: options.mixerUrl,
        width: options.width,
        height: options.height
      }
    }, function (error, response) {
      if (error) {
        console.error('Error on publishVideo: ' + JSON.stringify(error));
      } else {
        that.room.emitEvent('stream-published', [
{
          stream: that
        }
]);
        if (options.rtmpUrl && options.bridge) {
          that.bridged = true;
        }
        that.processSdpAnswer(response.sdpAnswer);
      }
    });
  };

  this.startVideoCallback = function (error, sdpOfferParam, wp) {
    if (error) {
      return console.error('(subscribe) SDP offer error: ' +
        JSON.stringify(error));
    }

    room.reportInfo('createOffer', sdpOfferParam);

    console.log('Sending SDP offer to subscribe to ' +
      that.getGlobalID(), sdpOfferParam);
    var options = {
      sdpOffer: sdpOfferParam,
      level: that.level
    };
    Object.keys(that.externalOptions).map(key => {
      options[key] = that.externalOptions[key];
    });

    kurento.sendRequest('subscribeStream', {
      sender: that.getGlobalID(),
      sdpOffer: sdpOfferParam,
      roomId: room.name,
      userId: room.user,
      streamId: that.getGlobalID(),
      options: options
    },
      function (error, response) {
        if (error) {
          console.error('Error on recvVideoFrom: ' + JSON.stringify(error));
        } else {
          that.processSdpAnswer(response.sdpAnswer);
        }
      }
    );
  };

  function initWebRtcPeer(sdpOfferCallback) {
    if (local) {
      var options = {
        sdpProcessor: function (sdp) {
          sdp.sdp = maybeSetVideoSendBitRate(sdp.sdp, that.options.bitrate);
          if (that.acodec) {
            sdp.sdp = maybePreferCodec(sdp.sdp, 'audio', 'send', that.acodec);
          }
          console.log(sdp.sdp);
          if (that.vcodec) {
            sdp.sdp = maybePreferCodec(sdp.sdp, 'video', 'send', that.vcodec); //
          }
          console.log(sdp.sdp);
          return sdp;
        },
        videoStream: wrStream,
        onicecandidate: function (candidate) {
          participant.sendIceCandidate(that.getGlobalID(), candidate, that.level);
        },
        oniceconnectionstatechange: function (state) {
          console.log('state changed : ' + JSON.stringify(state) + ' state: ' + wp.peerConnection.iceConnectionState);
        },
        //
        mediaConstraints: {
          audio: that.options.recvAduio,
          video: that.options.recvVideo
        },
        configuration: {
          iceServers: room.iceservers,
          rtcpMuxPolicy: 'require'
        }
      };
      if (that.displayMyRemote()) {
        console.log(new Date().getSeconds() + '.' + new Date().getMilliseconds());
        wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, function (error) {
          if (error) {
            return console.error(error);
          }
          wp.peerConnection.oniceconnectionstatechange = function (state) {
            var iceMessage = new Date().getSeconds() + '.' + new Date().getMilliseconds() + ' state changed : ' + JSON.stringify(state) +
              'state: ' + wp.peerConnection.iceConnectionState;
            room.reportInfo('iceStateChange', iceMessage);
            console.log(iceMessage);
            if (wp.peerConnection.iceConnectionState === 'failed') {
              ee.emitEvent('connection-failed', [{stream: that}]);
            }
          };
          wp.peerConnection.onsignalingstatechange = function (event) {
            room.reportInfo('signalingsStateChange', wp.peerConnection.signalingState);
          };
          this.generateOffer(sdpOfferCallback.bind(that));
        });
      } else {
        console.log(new Date().getSeconds() + '.' + new Date().getMilliseconds());
        wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, function (error) {
          if (error) {
            return console.error(error);
          }
          wp.peerConnection.oniceconnectionstatechange = function (state) {
            var iceMessage = new Date().getSeconds() + '.' + new Date().getMilliseconds() + ' state changed : ' + JSON.stringify(state) +
              ' state: ' + wp.peerConnection.iceConnectionState;
            room.reportInfo('iceStateChange', iceMessage);
            console.log(iceMessage);
            if (wp.peerConnection.iceConnectionState === 'failed') {
              ee.emitEvent('connection-failed', [{stream: that}]);
            }
          };
          wp.peerConnection.onsignalingstatechange = function (event) {
            room.reportInfo('signalingsStateChange', wp.peerConnection.signalingState);
          };
          this.generateOffer(sdpOfferCallback.bind(that));
        });
      }

    } else {
      var offerConstraints = {
        mandatory: {
          OfferToReceiveVideo: that.options.recvVideo,
          OfferToReceiveAudio: that.options.recvAudio
        }
      };
      console.log('Constraints of generate SDP offer (subscribing)',
        offerConstraints);
      var audio = true;
      if (that.externalOptions.audio == false) {
        audio = false;
      }
      var options = {
        sdpProcessor: function (sdp) {
          sdp.sdp = maybeSetVideoSendBitRate(sdp.sdp, that.options.bitrate);
          if (that.acodec) {
            sdp.sdp = maybePreferCodec(sdp.sdp, 'audio', 'send', that.acodec);
          }
          if (that.vcodec) {
            sdp.sdp = maybePreferCodec(sdp.sdp, 'video', 'send', that.vcodec); //
          }
          return sdp;
        },
        onicecandidate: function (candidate) {
          participant.sendIceCandidate(that.getGlobalID(), candidate, that.level);
        },
        onnectionConstraints: offerConstraints,
        mediaConstraints: {
          audio: audio,
          video: that.options.recvVideo
        },
        configuration: {
          iceServers: room.iceservers,
          rtcpMuxPolicy: 'require'
        }
      };
      wp = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function (error) {
        if (error) {
          return console.error(error);
        }
        wp.peerConnection.oniceconnectionstatechange = function (state) {
          console.log(JSON.stringify(state) + ' state: ' + wp.peerConnection.iceConnectionState);
          if (wp.peerConnection.iceConnectionState === 'failed') {
            ee.emitEvent('connection-failed', [{stream: that}]);
          }
        };
        this.generateOffer(sdpOfferCallback.bind(that));
      });
    }
    console.log('Waiting for SDP offer to be generated (' +
      (local ? 'local' : 'remote') + ' peer: ' + that.getGlobalID() + ')');
  }

  this.publish = function (options) {

    options = options || {};
    // FIXME: Throw error when stream is not local
    if (options.acodec) {
      that.acodec = options.acodec;
    }
    if (options.vcodec) {
      that.vcodec = options.vcodec;
    }
    that.externalOptions = options;
    initWebRtcPeer(that.publishVideoCallback);

    // FIXME: Now we have coupled connecting to a room and adding a
    // stream to this room. But in the new API, there are two steps.
    // This is the second step. For now, it do nothing.

  };

  this.subscribe = function (options) {

    // FIXME: In the current implementation all participants are subscribed
    // automatically to all other participants. We use this method only to
    // negotiate SDP
    options = options || {};
    that.externalOptions = options;
    if (options.bypass) {
      that.level = options.bypass;
    }

    initWebRtcPeer(that.startVideoCallback);
  };

  this.processSdpAnswer = function (sdpAnswer) {
    var answer = new RTCSessionDescription({
      type: 'answer',
      sdp: sdpAnswer
    });
    console.log(that.getGlobalID() + ': set peer connection with recvd SDP answer',
      sdpAnswer);
    var participantId = that.getGlobalID();
    var pc = wp.peerConnection;
    pc.setRemoteDescription(answer, function () {
      // Avoids to subscribe to your own stream remotely 
      // except when showMyRemote is true
      if (!local) {
        wrStream = pc.getRemoteStreams()[0];
        console.log('Peer remote stream', wrStream);
        that.wrStream = wrStream;
        that.room.emitEvent('stream-added', [
{
          stream: that
        }
]);
      }

      that.room.reportInfo('recvOffer', answer);
    }, function (error) {
      console.error(that.getGlobalID() + ': Error setting SDP to the peer connection: ' +
        JSON.stringify(error));
    });
  };

  this.unpublish = function () {
    if (wp) {
      wp.dispose();
    } else if (wrStream) {
        wrStream.getAudioTracks().forEach(function (track) {
          track.stop && track.stop();
        });
        wrStream.getVideoTracks().forEach(function (track) {
          track.stop && track.stop();
        });
        speechEvent.stop();
      }

    kurento.sendRequest('unpublishStream', {
      roomId: room.name,
      userId: room.user,
      streamId: that.getGlobalID()
    }, function (error, response) {
      if (error) {
        console.error(error);
      } else {
        that.room.emitEvent('stream-unpublished', [
{
          stream: that
        }
]);
        console.log(that.getGlobalID() + ': Stream \'' + id + '\' unpublished');
      }
    });
  };

  this.dispose = function () {
    if (wp) {
      wp.dispose();
    } else if (wrStream) {
        wrStream.getAudioTracks().forEach(function (track) {
          track.stop && track.stop();
        });
        wrStream.getVideoTracks().forEach(function (track) {
          track.stop && track.stop();
        });
      }

    console.log(that.getGlobalID() + ': Stream \'' + id + '\' disposed');
  };
}

// KurentoRoom --------------------------------

function KurentoRoom(wsUri, callback) {
  if (!(this instanceof KurentoRoom)) return new KurentoRoom(wsUri, callback);

  var that = this;
  var room = undefined;
  var times = 0;
  var userName;
  var sessionId = undefined;

  var rpc = new RpcBuilder.clients.JsonRpcClient({
    rpc: {
      joinUser: onParticipantJoined,
      publishStream: onParticipantPublished,
      unpublishStream: onParticipantUnpublished,
      leaveUser: onParticipantLeft,
      participantEvicted: onParticipantEvicted,
      sendMessage: onNewMessage,
      onIceCandidate: iceCandidateEvent,
      roomClosed: onRoomClosed,
      mediaError: onMediaError,
      streamDisconnected: onStreamDisconnect,
      analyzeData: onAnalyzeData,
      startAnalyze: onStartAnalyze,
      stopAnalyze: onStopAnalyze,
      analyzeError: onAnalyzeError
    },
    heartbeat: 3000,
    sendCloseMessage: false,
    ws: {
      uri: wsUri,
      useSockJS: false,
      onpingparams: function () {
        return {
          sessionId: sessionId
        };
      },
      onconnected: function () {
        //self.state = 'connected';
        //self.sendRequests();
        times = 0;
        callback(null, that);
      },
      ondisconnect: function () {
        times++;
        console.log('disconnect ' + times);
        if (times > 0) {
          onRoomClosed();
        }
      },
      onreconnecting: function () {
        onReconnecting();
      },
      onreconnected: function () {
        times = 0;
        //
        console.log('reconnected');
      }
    }
  });
  function onReconnecting () {
    if (room !== undefined) room.onReconnecting();
  }
  function onParticipantJoined(msg) {
    if (room !== undefined) {
      room.onParticipantJoined(msg);
    }
  }

  function onStreamDisconnect(msg) {
    if (room != undefined) {
      room.onStreamDisconnect(msg);
    }
  }
  function onParticipantPublished(msg) {
    if (room !== undefined) {
      room.onParticipantPublished(msg);
    }
  }

  function onParticipantUnpublished(msg) {
    if (room !== undefined) {
      room.onParticipantUnpublished(msg);
    }
  }

  function onParticipantLeft(msg) {
    if (room !== undefined) {
      room.onParticipantLeft(msg);
    }
  }

  function onParticipantEvicted(msg) {
    if (room !== undefined) {
      room.onParticipantEvicted(msg);
    }
  }

  function onNewMessage(msg) {
    if (room !== undefined) {
      room.onNewMessage(msg);
    }
  }

  function iceCandidateEvent(msg) {
    if (room !== undefined) {
      room.recvIceCandidate(msg);
    }
  }

  function onRoomClosed(msg) {
    if (room !== undefined) {
      msg = msg || {};
      msg.room = msg.room || room.name;
      room.onRoomClosed(msg);
    }
  }

  function onMediaError(params) {
    if (room !== undefined) {
      room.onMediaError(params);
    }
  }

  function onAnalyzeData(msg) {
    if (room !== undefined) {
      room.onAnalyzeData(msg);
    }
  }

  function onStartAnalyze(msg) {
    if (room !== undefined) {
      room.onStartAnalyze(msg);
    }
  }

  function onStopAnalyze(msg) {
    if (room !== undefined) {
      room.onStopAnalyze(msg);
    }
  }

  function onAnalyzeError(msg) {
    if (room !== undefined) {
      room.onAnalyzeError(msg);
    }
  }

  var rpcParams;

  this.setRpcParams = function (params) {
    rpcParams = params;
  };

  this.sendRequest = function (method, params, callback) {
    if (rpcParams && rpcParams !== 'null' && rpcParams !== 'undefined') {
      for (var index in rpcParams) {
        if (rpcParams.hasOwnProperty(index)) {
          params[index] = rpcParams[index];
        }
      }
    }
    if (!params.sessionId) params.sessionId = sessionId;

    // rpc.encode(method, params, callback);
    rpc.send(method, params, function (error, response) {
      if (!sessionId && !error && response.sessionId) sessionId = response.sessionId;
      callback(error, response);
    });
    console.log('Sent request: { method:"' + method + '", params: ' +
      JSON.stringify(params) + ' }');
  };

  this.close = function (forced) {
    if (room !== undefined) {
      room.leave(forced);
    }
    rpc.close();
  };

  this.disconnectParticipant = function (stream) {
    if (room !== undefined) {
      room.disconnect(stream);
    }
  };

  this.Stream = function (room, options) {
    options.participant = room.getLocalParticipant();
    options.room = room.name;
    options.user = options.participant.getID();
    options.bridge = options.bridge || room.options.bridge;
    return new Stream(that, true, room, options);
  };

  this.Room = function (options) {
    // FIXME Support more than one room
    room = new Room(that, options);
    // FIXME Include name in stream, not in room
    userName = options.userName;
    return room;
  };

  //CHAT
  this.sendMessage = function (room, user, message) {
    this.sendRequest('sendMessage', {
      message: message,
      userMessage: user,
      roomMessage: room,
      roomId: room,
      userId: user,
      msg: message
    }, function (error, response) {
      if (error) {
        console.error(error);
      } else {
        connected = false;
      }
    });
  };

  this.sendCustomRequest = function (params, callback) {
    this.sendRequest('customRequest', params, callback);
  };

}


